Skip to content

Unstructure subclassed built-in collections like their built-ins - #824

Closed
Aschenbath wants to merge 1 commit into
python-attrs:mainfrom
Aschenbath:fix-287-subclassed-builtins
Closed

Aschenbath wants to merge 1 commit into
python-attrs:mainfrom
Aschenbath:fix-287-subclassed-builtins

Conversation

@Aschenbath

@Aschenbath Aschenbath commented Nov 4, 2026 •

Copy link
Copy Markdown

Fixes #287.

Subclasses of built-in collections were not un/structured like their built-ins: converter.unstructure(MyList([Item(1)])) returned the raw Item objects, and a field annotated MyList[Item] was passed through unchanged too, while list[Item] worked.

Two gaps in the type predicates in cattrs._compat:

  1. The collection predicates (is_sequence, is_mutable_sequence, is_tuple, is_deque, is_mutable_set, is_frozenset, is_mapping) only recognized typing._GenericAlias. Subscripting a user class produces a PEP 585 types.GenericAlias instead, so MyList[Item] never matched. These predicates now accept both alias types (as is_generic already did).
  2. Unlike is_mapping, the sequence and set predicates had no clause for bare subclasses, so dispatching on a bare MyList class (for example when unstructuring an instance directly) fell through to the identity fallback. is_mutable_sequence, is_mutable_set and is_frozenset now also match bare subclasses of list/deque, set and frozenset respectively, mirroring the existing is_mapping clause.

Class-registered hooks (for example for str, bytes or Path subclasses) still take precedence, since singledispatch runs before these predicates, and attrs classes are still matched earlier by the has predicate.

Tests:

  • Added test_unstructure_subclassed_list and test_unstructure_subclassed_sets to tests/test_baseconverter.py, covering bare instances, bare and subscripted field annotations, and a structuring round-trip.
  • Regression runs: tests/test_baseconverter.py, tests/test_gen_dict.py, tests/test_dicts.py, tests/test_function_dispatch.py, tests/test_multistrategy_dispatch.py, tests/test_tuples.py, tests/test_structure.py, tests/test_unstructure.py, tests/test_unstructure_collections.py, tests/test_cols.py, tests/test_defaultdicts.py, tests/test_typeddicts.py — all passed (257 tests).
  • ruff check and ruff format --check on the changed files: pass.

A changelog entry is included in HISTORY.md.

@Aschenbath

Copy link
Copy Markdown
Author

Closing this one myself: I opened too many PRs here in a short time, which isn't fair to your review queue — sorry about that. I'm leaving #827 and #822 open. If this fix is something you'd want, I'm happy to reopen or rebase it later.

@Aschenbath Aschenbath closed this Nov 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unstructuring subclassed built-ins

1 participant